/*
 * Copyright (c) 2002 - 2014 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */

/*
*  Copyright (c) 2013,
*      Tobias Blaschke <code@tobiasblaschke.de>
*  All rights reserved.

*  Redistribution and use in source and binary forms, with or without
*  modification, are permitted provided that the following conditions are met:
*
*  1. Redistributions of source code must retain the above copyright notice,
*     this list of conditions and the following disclaimer.
*
*  2. Redistributions in binary form must reproduce the above copyright notice,
*     this list of conditions and the following disclaimer in the documentation
*     and/or other materials provided with the distribution.
*
*  3. The names of the contributors may not be used to endorse or promote
*     products derived from this software without specific prior written
*     permission.
*
*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
*  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
*  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
*  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
*  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
*  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
*  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
*  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
*  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
*  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
*  POSSIBILITY OF SUCH DAMAGE.
*/
package com.ibm.wala.core.util.ssa;

import com.ibm.wala.core.util.ssa.SSAValue.NamedKey;
import com.ibm.wala.core.util.ssa.SSAValue.VariableKey;
import com.ibm.wala.core.util.strings.Atom;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.collections.HashMapFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Manage SSA-Variables in synthetic methods.
 *
 * @author Tobias Blaschke &lt;code@tobiasblaschke.de&gt;
 * @since 2013-09-19
 */
public class SSAValueManager {

  private static final boolean DEBUG = false;
  private final boolean AUTOMAKE_NAMES = true;

  private enum ValueStatus {
    /** Value has never been mentioned before */
    UNUSED,
    /** Awaiting to be set using setAllocation */
    UNALLOCATED,
    /** Set and ready to use */
    ALLOCATED,
    /** Has to be assigned using a Phi-Instruction */
    FREE,
    /** Should only be used as argument to a Phi Instruction */
    INVALIDATED,
    /** Should not be referenced any more */
    CLOSED,
    /** Well FREE and INVALIDATED */
    FREE_INVALIDATED,
    /** Well FREE and CLOSED */
    FREE_CLOSED
  }

  // TODO: nextLocal may be 0 on getUnmanaged!
  /** The next variable not under management yet */
  private int nextLocal;

  /** for managing cascaded code blocks */
  private int currentScope = 0;

  /** Description only used for toString() */
  private final String description;

  private final MethodReference forMethod;

  /** User-Defined debugging info */
  public String breadCrumb = "";

  private static class Managed<T extends SSAValue> {
    public ValueStatus status = ValueStatus.UNUSED;
    public SSAInstruction setBy = null;
    public int setInScope = -1;
    public final VariableKey key;
    public final T value;

    public Managed(final T value, final VariableKey key) {
      this.value = value;
      this.key = key;
    }

    @Override
    public String toString() {
      return "<Managed "
          + this.value
          + " key=\""
          + this.key
          + "\" status=\""
          + this.status
          + " setIn=\""
          + this.setInScope
          + "\" setBy=\""
          + this.setBy
          + "\" />";
    }
  }

  /** The main data-structure of the management */
  private final Map<VariableKey, List<Managed<? extends SSAValue>>> seenTypes =
      HashMapFactory.make();

  private final List<SSAValue> unmanaged = new ArrayList<>();

  public SSAValueManager(ParameterAccessor acc) {
    this.nextLocal = acc.getFirstAfter();
    this.description = " based on ParameterAccessor " + acc;
    this.forMethod = acc.forMethod();

    for (SSAValue val : acc.all()) {
      setAllocation(val, null);
    }
  }

  /*
  public SSAValueManager(final MethodReference forMethod) {
      this (new
      //this.nextLocal = nextLocal;
      this.description = " stand alone";
      this.forMethod = forMethod;
  }*/

  /**
   * Register a variable _after_ allocation.
   *
   * <p>The proper way to add an allocation is to get a Variable using {@link #getUnallocated}. Then
   * assign it a value. And at last call this function.
   *
   * <p>You can however directly call the function if the type has not been seen before.
   *
   * @param value an unallocated SSA-Variable to assign the allocation to
   * @param setBy The instruction that set the value (optional)
   * @throws IllegalStateException if you set more than one allocation for that type (TODO better
   *     check!)
   * @throws IllegalArgumentException if type is null or ssaValue is zero or negative
   */
  public void setAllocation(SSAValue value, SSAInstruction setBy) {
    if (value == null) {
      throw new IllegalArgumentException("The SSA-Variable may not be null");
    }

    if (seenTypes.containsKey(value.key)) {
      for (Managed<? extends SSAValue> param : seenTypes.get(value.key)) {
        if (param.status == ValueStatus.UNALLOCATED) {
          // XXX: Allow more?
          assert param.value.getType().equals(value.getType()) : "Inequal types";

          if ((param.value.getNumber() + 1) > nextLocal) {
            nextLocal = param.value.getNumber() + 1;
          }

          debug("reSetting SSA {} to allocated", value);
          param.status = ValueStatus.ALLOCATED;
          param.setInScope = currentScope;
          param.setBy = setBy;

          return;
        }
      }
      { // DEBUG
        System.out.println("Keys for " + value + ':');
        for (Managed<? extends SSAValue> param : seenTypes.get(value.key)) {
          System.out.println("\tKey " + param.key + "\t=>" + param.status);
        }
      } // */
      throw new IllegalStateException(
          "The parameter " + value + " using Key " + value.key + " has already been allocated");
    } else {
      info("New variable in management: {}", value);
      final Managed<SSAValue> param = new Managed<>(value, value.key);
      param.status = ValueStatus.ALLOCATED;
      param.setInScope = currentScope;
      param.setBy = setBy;

      final List<Managed<? extends SSAValue>> aParam = new ArrayList<>();
      aParam.add(param);

      seenTypes.put(value.key, aParam);
    }
  }

  /**
   * Register a Phi-Instruction _after_ added to the model.
   *
   * @param value the number the SSA-Instruction assigns to
   * @param setBy the Phi-Instruction itself - may be null
   * @throws IllegalArgumentException if you assign to a number requested using {@link #getFree} but
   *     types mismatch.
   * @throws IllegalStateException if you forgot to close some Phis
   */
  public void setPhi(final SSAValue value, SSAInstruction setBy) {
    if (value == null) {
      throw new IllegalArgumentException("The SSA-Variable may not be null.");
    }

    boolean didPhi = false;

    if (seenTypes.containsKey(value.key)) {
      for (Managed<? extends SSAValue> param : seenTypes.get(value.key)) {
        if ((param.status == ValueStatus.FREE)
            || (param.status == ValueStatus.FREE_INVALIDATED)
            || (param.status == ValueStatus.FREE_CLOSED)) {
          // XXX: Allow more?
          assert param.value.getType().equals(value.getType()) : "Unequal types";
          if (param.value.getNumber() != value.getNumber()) {
            if ((param.status == ValueStatus.FREE) && (param.setInScope == currentScope)) {
              param.status = ValueStatus.FREE_CLOSED;
            }
            continue;
          }

          if (param.status == ValueStatus.FREE) {
            param.status = ValueStatus.ALLOCATED;
          } else if (param.status == ValueStatus.FREE_INVALIDATED) {
            param.status = ValueStatus.INVALIDATED;
          } else if (param.status == ValueStatus.FREE_CLOSED) {
            param.status = ValueStatus.CLOSED;
          }
          param.setInScope = currentScope;
          param.setBy = setBy;

          info("Setting SSA {} to phi! now {}", value, param.status);
          didPhi = true;
        } else if (param.setInScope == currentScope) {
          if (param.status == ValueStatus.INVALIDATED) {
            info("Closing SSA Value {} in scope {}", param.value, param.setInScope);
            param.status = ValueStatus.CLOSED;
          } else if (param.status == ValueStatus.FREE_INVALIDATED) { // TODO: FREE CLOSED
            info("Closing free SSA Value {} in scope {}", param.value, param.setInScope);
            param.status = ValueStatus.FREE_CLOSED;
          }
        }
        //        else if (param.setInScope < currentScope) {
        //          // param.status = ValueStatus.INVALIDATED;
        //        } else {
        //          // TODO: NO! I JUST WANTED TO ADD THEM! *grrr*
        //          // error("MISSING PHI for "
        //          // throw new IllegalStateException("You forgot Phis in subordinate blocks");
        //        }
      }
      assert didPhi;
    } else {
      throw new IllegalStateException("This should not be reached!");
    }
  }

  /**
   * Returns and registers a free SSA-Number to a Type.
   *
   * <p>You have to set the type using a Phi-Instruction. Also you don't have to add that
   * instruction immediately it is required that it is added before the Model gets finished.
   *
   * <p>You can request the List of unmet Phi-Instructions by using XXX
   *
   * @return an unused SSA-Number
   * @throws IllegalArgumentException if type is null
   */
  public SSAValue getFree(TypeReference type, VariableKey key) {
    if (type == null) {
      throw new IllegalArgumentException("The argument type may not be null");
    }
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    final SSAValue var = new SSAValue(nextLocal++, type, this.forMethod, key);
    final Managed<SSAValue> param = new Managed<>(var, key);

    param.status = ValueStatus.FREE;
    param.setInScope = currentScope;

    if (seenTypes.containsKey(key)) {
      seenTypes.get(key).add(param);
    } else {
      List<Managed<? extends SSAValue>> aParam = new ArrayList<>();
      aParam.add(param);

      seenTypes.put(key, aParam);
    }

    debug("Returning as Free SSA: {}", param);
    return var;
  }

  /**
   * Get an unused number to assign to.
   *
   * <p>There may only be one unallocated value for each type at a time. XXX: Really?
   *
   * @return SSA-Variable
   * @throws IllegalStateException if there is already an unallocated variable of that type
   * @throws IllegalArgumentException if type is null
   */
  public SSAValue getUnallocated(TypeReference type, VariableKey key) {
    if (type == null) {
      throw new IllegalArgumentException("The argument type may not be null");
    }
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    if (seenTypes.containsKey(key)) {
      for (Managed<? extends SSAValue> p : seenTypes.get(key)) {
        if (p.status == ValueStatus.UNALLOCATED) {
          throw new IllegalStateException(
              "There may be only one unallocated instance to a kay (" + key + ") at a time");
        }
      }
    }

    final SSAValue var = new SSAValue(nextLocal++, type, this.forMethod, key);
    final Managed<SSAValue> param = new Managed<>(var, key);

    param.status = ValueStatus.UNALLOCATED;
    param.setInScope = currentScope;

    if (seenTypes.containsKey(key)) {
      seenTypes.get(key).add(param);
    } else {
      List<Managed<? extends SSAValue>> aParam = new ArrayList<>();
      aParam.add(param);

      seenTypes.put(key, aParam);
    }

    debug("Returning as Unallocated SSA: {}", param);
    return var;
  }

  /**
   * Retrieve a SSA-Value that is not under management.
   *
   * <p>Use instead of 'nextLocal++', else SSA-Values will clash!
   *
   * @return SSA-Variable
   */
  public SSAValue getUnmanaged(TypeReference type, VariableKey key) {
    final SSAValue var = new SSAValue(nextLocal++, type, this.forMethod, key);
    this.unmanaged.add(var);
    return var;
  }

  public SSAValue getUnmanaged(TypeReference type, String name) {
    return getUnmanaged(type, new NamedKey(type.getName(), name));
  }

  /**
   * Retrieve the SSA-Number that is valid for a type in the current scope.
   *
   * <p>Either that number origins from an allocation or a PhiInstruction (to be).
   *
   * @return a ssa number
   * @throws IllegalStateException if no number is assignable
   * @throws IllegalArgumentException if type was not seen before or is null
   */
  public SSAValue getCurrent(VariableKey key) {
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    Managed<? extends SSAValue> candidate = null;

    if (seenTypes.containsKey(key)) {
      for (Managed<? extends SSAValue> param : seenTypes.get(key)) {
        if ((param.status == ValueStatus.FREE) || (param.status == ValueStatus.ALLOCATED)) {
          // assert (param.value.getType().equals(type)) : "Unequal types";
          if (param.setInScope > currentScope) {
            debug("SSA Value {} is out of scope {}", param, currentScope);
          } else if (param.setInScope == currentScope) {
            debug("Returning SSA Value {} is {}", param.value, param.status);
            return param.value;
          } else {
            if ((candidate == null) || (param.setInScope > candidate.setInScope)) {
              candidate = param;
            }
          }
        } else {
          debug("SSA Value {} is {}", param, param.status);
        }
      }
    } else {
      throw new IllegalArgumentException(
          "Key " + key + " has never been seen before! Known keys are " + seenTypes.keySet());
    }

    if (candidate != null) {
      debug("Returning inherited (from {}) SSA Value {}", candidate.setInScope, candidate);
      return candidate.value;
    } else {
      throw new IllegalStateException("No suitable candidate has been found for Key " + key);
    }
  }

  /**
   * Retrieve the SSA-Number that is valid for a type in the super-ordinate scope.
   *
   * <p>Either that number origins from an allocation or a PhiInstruction (to be).
   *
   * @return a ssa number
   * @throws IllegalStateException if no number is assignable
   * @throws IllegalArgumentException if type was not seen before or is null
   */
  public SSAValue getSuper(VariableKey key) {
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    final SSAValue cand;
    currentScope--;
    assert (currentScope >= 0);
    cand = getCurrent(key);
    currentScope++;
    return cand;
  }

  /**
   * Returns all "free" and "allocated" variables and the invalid ones in a sub-scope.
   *
   * <p>This is a suggestion which variables to consider as parameter to a Phi-Function.
   *
   * @throws IllegalArgumentException if type was not seen before or is null
   */
  public List<SSAValue> getAllForPhi(VariableKey key) {
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    List<SSAValue> ret = new ArrayList<>();

    if (seenTypes.containsKey(key)) {
      for (Managed<? extends SSAValue> param : seenTypes.get(key)) {
        if ((param.status == ValueStatus.FREE) || (param.status == ValueStatus.ALLOCATED)) {
          // assert (param.type.equals(type)) : "Unequal types";

          ret.add(param.value);
        } else if ((param.status == ValueStatus.INVALIDATED) && param.setInScope > currentScope) {

          ret.add(param.value);
        }
      }
    } else {
      throw new IllegalArgumentException("Key " + key + " has never been seen before!");
    }

    return ret;
  }

  /**
   * Return if the type is managed by this class.
   *
   * @param withSuper when true return true if a managed key may be cast to type, when false type
   *     has to match exactly
   * @param key the type in question
   * @throws IllegalArgumentException if key is null
   */
  public boolean isSeen(VariableKey key, boolean withSuper) {
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    if (withSuper) {
      return seenTypes.containsKey(key);
    } else {
      if (seenTypes.containsKey(key)) {
        /*                if (seenTypes.get(key).get(0).type.equals(type)) {    // TODO: Rethink
            return true;
        }*/
      }
      return false;
    }
  }

  /**
   * Return if the type is managed by this class.
   *
   * <p>This variant respects super-types. Use isSeen(VariableKey, boolean) with a setting for
   * withSuper of false to enforce exact matches.
   *
   * @return if the type is managed by this class.
   */
  public boolean isSeen(VariableKey key) {
    return isSeen(key, true);
  }

  /**
   * Returns if an instance for that type needs to be allocated.
   *
   * <p>However this function does not respect weather a PhiInstruction is needed.
   *
   * @throws IllegalArgumentException if type is null
   */
  public boolean needsAllocation(VariableKey key) {
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    if (seenTypes.containsKey(key)) {
      if (seenTypes.get(key).size() > 1) { // TODO INCORRECT may all be UNALLOCATED
        return false;
      } else {
        return (seenTypes.get(key).get(0).status == ValueStatus.UNALLOCATED);
      }
    } else {
      return true;
    }
  }

  /**
   * Returns if a PhiInstruction (still) has to be added.
   *
   * <p>This is true if the Value has changed in a deeper scope, has been invalidated or requested
   * using getFree
   *
   * @throws IllegalArgumentException if type is null or has not been seen before
   */
  public boolean needsPhi(VariableKey key) {
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    boolean seenLive = false;

    if (seenTypes.containsKey(key)) {

      for (Managed<? extends SSAValue> param : seenTypes.get(key)) { // TODO: Check all these
        if ((param.status == ValueStatus.FREE)) { // TODO: What about scopes
          return true;
        }

        if (param.status == ValueStatus.ALLOCATED) {
          if (seenLive) {
            return true;
          } else {
            seenLive = true;
          }
        }
      }
    } else {
      throw new IllegalArgumentException("Key " + key + " has never been seen before!");
    }

    throw new IllegalStateException("No suitable candidate has been found"); // TODO WRONG text
  }

  /**
   * Marks all known instances of VariableKey invalid.
   *
   * <p>A call to this method is useful before a call to setAllocation. This methods sets all known
   * instances to invalid, setAllocation will assign the new "current" instance to use.
   *
   * @param key Which variables to invalidate.
   * @throws IllegalArgumentException if type was not seen before or is null
   */
  public void invalidate(VariableKey key) {
    if (key == null) {
      throw new IllegalArgumentException("The argument key may not be null");
    }

    if (seenTypes.containsKey(key)) {
      for (Managed<? extends SSAValue> param : seenTypes.get(key)) {
        if ((param.status != ValueStatus.CLOSED)
            && (param.status != ValueStatus.FREE_CLOSED)
            && (param.status != ValueStatus.FREE_INVALIDATED)
            && (param.status != ValueStatus.INVALIDATED)
            && (param.setInScope == currentScope)) {
          // assert(param.type.equals(type));

          if (param.status == ValueStatus.FREE) {
            param.status = ValueStatus.FREE_INVALIDATED;
          } else {
            param.status = ValueStatus.INVALIDATED;
          }
          info("Invalidated SSA {} for key {}", param, key);
        }
      }
    }
  }

  /**
   * Enter a subordinate scope.
   *
   * <p>Call this whenever a new code block starts i.e. when ever you would have to put a left
   * curly-bracket in the java code.
   *
   * <p>This function influences the placement of Phi-Functions. Thus if you don't change values you
   * don't have to call it.
   *
   * @param doesLoop set to true if the scope is introduced for a loop
   * @return The depth
   */
  public int scopeDown(boolean doesLoop) { // TODO: Rename scopeInto
    // TODO: Delete Parameters if there already was scopeNo
    currentScope++;
    return currentScope;
  }

  /**
   * Leave a subordinate scope.
   *
   * <p>All changes are marked invalid thus to be expected to be collected by a PhiInstruction.
   *
   * @throws IllegalStateException if already at top level
   */
  public int scopeUp() { // TODO: Rename scopeOut
    // First: Invalidate changed values
    for (List<Managed<? extends SSAValue>> plist : seenTypes.values()) {
      for (Managed<? extends SSAValue> param : plist) {
        if (param.setInScope == currentScope) {
          invalidate(param.value.key);
        } else if ((param.setInScope > currentScope)
            && ((param.status != ValueStatus.INVALIDATED)
                && (param.status != ValueStatus.CLOSED))) {
          throw new IllegalStateException(
              "A parameter was in wrong status when leaving a sub-subordinate scope: "
                  + param
                  + " should have been invalidated or closed by an other scope.");
        }
      }
    }
    currentScope--;
    return currentScope;
  }

  @Override
  public String toString() {
    return "<AndroidModelParameterManager " + this.description + '>';
  }

  /**
   * Create new SSAValue with UniqueKey and Exception-Type.
   *
   * <p>The generated SSAValue will be unmanaged. It is mainly useful for SSAInvokeInstructions.
   *
   * @return new unmanaged SSAValue with Exception-Type
   */
  public SSAValue getException() {
    SSAValue exc =
        new SSAValue(
            nextLocal++,
            TypeReference.JavaLangException,
            this.forMethod,
            "exception_" + nextLocal); // UniqueKey
    this.unmanaged.add(exc);
    return exc;
  }

  /** Collect the variable-names of all known variables. */
  public Map<Integer, Atom> makeLocalNames() {
    final Map<Integer, Atom> names = new HashMap<>();
    final Map<VariableKey, Integer> suffix = new HashMap<>();
    int currentSuffix = 0;

    for (final List<Managed<? extends SSAValue>> managedValues : seenTypes.values()) {
      for (final Managed<? extends SSAValue> managed : managedValues) {
        final SSAValue val = managed.value;
        final String name = val.getVariableName();
        if (name != null) {
          final Atom nameAtom = Atom.findOrCreateAsciiAtom(name);
          names.put(val.getNumber(), nameAtom);
        } else if (AUTOMAKE_NAMES) {
          @SuppressWarnings("NonConstantStringShouldBeStringBuffer")
          String autoName = val.getType().getName().toString();
          if (autoName.contains("/")) {
            autoName = autoName.substring(autoName.lastIndexOf('/') + 1);
          }
          if (autoName.contains("$")) {
            autoName = autoName.substring(autoName.lastIndexOf('$') + 1);
          }
          autoName = autoName.replace("[", "Ar");
          final int mySuffix;
          if (suffix.containsKey(val.key)) {
            mySuffix = suffix.get(val.key);
          } else {
            mySuffix = currentSuffix++;
            suffix.put(val.key, mySuffix);
          }
          autoName = 'm' + autoName + '_' + mySuffix;
          final Atom nameAtom = Atom.findOrCreateAsciiAtom(autoName);
          names.put(val.getNumber(), nameAtom);
        }
      }
    }

    for (final SSAValue val : this.unmanaged) {
      final String name = val.getVariableName();
      if (name != null) {
        final Atom nameAtom = Atom.findOrCreateAsciiAtom(name);
        names.put(val.getNumber(), nameAtom);
      } else if (AUTOMAKE_NAMES) {
        @SuppressWarnings("NonConstantStringShouldBeStringBuffer")
        String autoName = val.getType().getName().toString();
        if (autoName.contains("/")) {
          autoName = autoName.substring(autoName.lastIndexOf('/') + 1);
        }
        if (autoName.contains("$")) {
          autoName = autoName.substring(autoName.lastIndexOf('$') + 1);
        }
        autoName = autoName.replace("[", "Ar");
        final int mySuffix;
        if (suffix.containsKey(val.key)) {
          mySuffix = suffix.get(val.key);
        } else {
          mySuffix = currentSuffix++;
          suffix.put(val.key, mySuffix);
        }
        autoName = 'm' + autoName + '_' + mySuffix;
        final Atom nameAtom = Atom.findOrCreateAsciiAtom(autoName);
        names.put(val.getNumber(), nameAtom);
      }
    }

    return names;
  }

  private static void debug(String s, Object... args) {
    if (DEBUG) {
      System.err.printf(s, args);
    }
  }

  private static void info(String s, Object... args) {
    if (DEBUG) {
      System.err.printf(s, args);
    }
  }
}
